package com.abso.mp3tunes.locker.core.services;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;

import com.abso.mp3tunes.locker.core.LockerServiceException;
import com.abso.mp3tunes.locker.core.data.AbstractLockerData;
import com.abso.mp3tunes.locker.core.data.Album;
import com.abso.mp3tunes.locker.core.data.Artist;
import com.abso.mp3tunes.locker.core.data.LockerDataUtils;
import com.abso.mp3tunes.locker.core.data.LoginData;
import com.abso.mp3tunes.locker.core.data.Playlist;
import com.abso.mp3tunes.locker.core.data.PlaylistData;
import com.abso.mp3tunes.locker.core.data.Track;

/**
 * The MP3tunes Locker service.
 */
public class LockerService extends AbstractLockerService {

	/** Default request chunk size. */
	public static final int DEFAULT_CHUNK_SIZE = 1000;

	/** The default value for the maximum search results */
	public static final int DEFAULT_MAX_SEARCH_RESULTS = 100;

	/** The current chunk size (default = DEFAULT_CHUNK_SIZE). */
	private int chunkSize = DEFAULT_CHUNK_SIZE;

	/**
	 * Array composed by the dom4j element class, reused by the <i>bind</i>
	 * method.
	 */
	private static final Class[] ELEM_CLASS_ARRAY = new Class[] { Element.class };

	/** The current login data. */
	private LoginData loginData;

	/**
	 * Constructs a a new locker service.
	 * 
	 * @param partnerToken
	 *            the partner token, which uniquely identifies and authenticates
	 *            all entities using API's to communicate with MP3tunes.com
	 * @param loginData
	 *            the login data resulted from a successful login attempt.
	 */
	public LockerService(String partnerToken, LoginData loginData) {
		super(partnerToken);
		this.loginData = loginData;
	}


	/**
	 * Sets the chunk size.
	 * 
	 * @param chunkSize   the chunk size.
	 * @see #DEFAULT_CHUNK_SIZE
	 */
	public void setChunkSize(int chunkSize) {
		this.chunkSize = chunkSize;
	}
	
	/**
	 * Returns the chunk size.
	 * 
	 * @return   the chunk size.
	 * @see #DEFAULT_CHUNK_SIZE
	 */
	protected int getChunkSize() {
		return chunkSize;
	}

	/**
	 * Returns the set of artists stored in the user's locker.
	 * 
	 * @return the set of artists.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Artist[] getArtists() throws LockerServiceException {
		List artists = new ArrayList();
		int set = 0;
		int totalResultSets = 0;
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "artist");
		params.put("count", Integer.toString(getChunkSize()));
		do {
			params.put("set", Integer.toString(set));
			Document doc = executeGeneral("lockerData", params);
			artists.addAll(bind(doc, "artistList/item", Artist.class));
			if (set == 0) {
				totalResultSets = getTotalResultSets(doc);
			}
			set++;
		} while (set < totalResultSets);
		return (Artist[]) artists.toArray(new Artist[0]);
	}

	/**
	 * Returns the set of albums stored in the user's locker.
	 * 
	 * @return the set of albums.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Album[] getAlbums() throws LockerServiceException {
		List albums = new ArrayList();
		int set = 0;
		int totalResultSets = 0;
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "album");
		params.put("count", Integer.toString(getChunkSize()));
		do {
			params.put("set", Integer.toString(set));
			Document doc = executeGeneral("lockerData", params);
			albums.addAll(bind(doc, "albumList/item", Album.class));
			if (set == 0) {
				totalResultSets = getTotalResultSets(doc);
			}
			set++;
		} while (set < totalResultSets);
		return (Album[]) albums.toArray(new Album[0]);
	}

	/**
	 * Returns the set of albums associated with a specific artist.
	 * 
	 * @param artistId
	 *            the identifier of an artist.
	 * @return the set of albums of the specified artist.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Album[] getAlbumsByArtist(String artistId)
			throws LockerServiceException {
		List albums = new ArrayList();
		int set = 0;
		int totalResultSets = 0;
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "album");
		params.put("artist_id", artistId);
		params.put("count", Integer.toString(getChunkSize()));
		do {
			params.put("set", Integer.toString(set));
			Document doc = executeGeneral("lockerData", params);
			albums.addAll(bind(doc, "albumList/item", Album.class));
			if (set == 0) {
				totalResultSets = getTotalResultSets(doc);
			}
			set++;
		} while (set < totalResultSets);
		return (Album[]) albums.toArray(new Album[0]);
	}

	/**
	 * Returns the set of playlists stored on the Locker. The returned array
	 * includes at least the three predefined playlists: recent, newest, and
	 * random tracks.
	 * 
	 * @return the set of playlists stored on the user's Locker.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Playlist[] getPlaylists() throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "playlist");
		Document doc = executeGeneral("lockerData", params);
		List playlists = bind(doc, "playlistList/item", Playlist.class);
		return (Playlist[]) playlists.toArray(new Playlist[0]);
	}

	/**
	 * Returns the set of tracks stored on the Locker.
	 * 
	 * @return the full set of tracks stored on the user's Locker.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Track[] getTracks() throws LockerServiceException {
		List tracks = new ArrayList();
		int set = 0;
		int totalResultSets = 0;
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "track");
		params.put("count", Integer.toString(getChunkSize()));
		do {
			params.put("set", Integer.toString(set));
			Document doc = executeGeneral("lockerData", params);
			tracks.addAll(bind(doc, "trackList/item", Track.class));
			if (set == 0) {
				totalResultSets = getTotalResultSets(doc);
			}
			set++;
		} while (set < totalResultSets);
		return (Track[]) tracks.toArray(new Track[0]);
	}

	/**
	 * Returns the set of tracks included on a specific playlist.
	 * 
	 * @param playlistId
	 *            the identifier of a playlist.
	 * @return the set of tracks associated with the specified playlist.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Track[] getTracksByPlaylist(String playlistId)
			throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "track");
		params.put("playlist_id", playlistId);
		Document doc = executeGeneral("lockerData", params);
		List tracks = bind(doc, "trackList/item", Track.class);
		return (Track[]) tracks.toArray(new Track[0]);
	}

	/**
	 * Returns the set of tracks included on a specific album.
	 * 
	 * @param albumId
	 *            the identifier of an album.
	 * @return the set of tracks associated with the specified playlist.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public Track[] getTracksByAlbum(String albumId)
			throws LockerServiceException {
		List tracks = new ArrayList();
		int set = 0;
		int totalResultSets = 0;
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("type", "track");
		params.put("album_id", albumId);
		params.put("count", Integer.toString(getChunkSize()));
		do {
			params.put("set", Integer.toString(set));
			Document doc = executeGeneral("lockerData", params);
			tracks.addAll(bind(doc, "trackList/item", Track.class));
			if (set == 0) {
				totalResultSets = getTotalResultSets(doc);
			}
			set++;
		} while (set < totalResultSets);
		return (Track[]) tracks.toArray(new Track[0]);
	}

	/**
	 * Performs a search over the Locker.
	 * 
	 * @param s
	 *            the case-insensitive string being serched.
	 * @param maxSearchResults
	 *            the maximum number of search results.
	 * @param searchArtists
	 *            if <code>true</code> the search is performed also over
	 *            artist names.
	 * @param searchAlbums
	 *            if <code>true</code> the search is performed also over album
	 *            titles.
	 * @param searchTracks
	 *            if <code>true</code> the search is performed also over track
	 *            titles.
	 * @return an array of <code>Album</code>, <code>Artist</code>, and
	 *         <code>Track</code> elements.
	 * @see Album
	 * @see Artist
	 * @see Track
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public AbstractLockerData[] search(String s, int maxSearchResults, boolean searchArtists,
			boolean searchAlbums, boolean searchTracks)
			throws LockerServiceException {
		if (maxSearchResults <= 0) {
			return new AbstractLockerData[0];
		}
		List items = new ArrayList();
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		List type = new ArrayList(3);
		if (searchArtists) {
			type.add("artist");
		}
		if (searchAlbums) {
			type.add("album");
		}
		if (searchTracks) {
			type.add("track");
		}
		params.put("type", StringUtils.join(type, ','));
		params.put("s", s);
		params.put("count", Integer.toString(maxSearchResults));
		params.put("set", "0");
		Document doc = executeGeneral("lockerSearch", params);
		if (searchArtists) {
			items.addAll(bind(doc, "artistList/item", Artist.class));
		}
		if (searchAlbums) {
			items.addAll(bind(doc, "albumList/item", Album.class));
		}
		if (searchTracks) {
			items.addAll(bind(doc, "trackList/item", Track.class));
		}
		return (AbstractLockerData[]) items.subList(0, Math.min(maxSearchResults, items.size())).toArray(new AbstractLockerData[0]);
	}

	/**
	 * Creates a new playlist.
	 * 
	 * @param title
	 *            the title of the playlist.
	 * @param trackFileKeys
	 *            the set of file keys of the tracks being included.
	 * @return the playlist data object storing information about the operation
	 *         result.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public PlaylistData createPlaylist(String title, String[] trackFileKeys)
			throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("title", title);
		params.put("name", title);
		params.put("keys", StringUtils.join(trackFileKeys, ","));
		Document doc = executeGeneral("playlistAdd", params);
		return new PlaylistData(doc.getRootElement());
	}

	/**
	 * Appends a track at the end of a playlist.
	 * 
	 * @param playlistId
	 *            the identifier of a playlist.
	 * @param trackFileKey
	 *            the file key of the track being appended.
	 * @return the playlist data object storing information about the operation
	 *         result.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public PlaylistData appendTrackToPlaylist(String playlistId,
			String trackFileKey) throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("playlist_id", playlistId);
		params.put("file_key", trackFileKey);
		Document doc = executeGeneral("playlistTrackAdd", params);
		return new PlaylistData(doc.getRootElement());
	}

	/**
	 * Removes a track from a playlist.
	 * 
	 * @param playlistId
	 *            the identifier of a playlist.
	 * @param trackFileKey
	 *            the file key of the track being removed.
	 * @return the playlist data object storing information about the operation
	 *         result.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public PlaylistData removeTrackFromPlaylist(String playlistId,
			String trackFileKey) throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("playlist_id", playlistId);
		params.put("file_key", trackFileKey);
		Document doc = executeGeneral("playlistTrackDelete", params);
		return new PlaylistData(doc.getRootElement());
	}

	/**
	 * Deletes a playlist.
	 * 
	 * @param playlistId
	 *            the identifier of a playlist.
	 * @return the playlist data object storing information about the operation
	 *         result.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public PlaylistData deletePlaylist(String playlistId)
			throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("playlist_id", playlistId);
		Document doc = executeGeneral("playlistDelete", params);
		return new PlaylistData(doc.getRootElement());
	}

	/**
	 * Renames a playlist.
	 * 
	 * @param playlistId
	 *            the identifier of a playlist.
	 * @param newTitle
	 *            the new title.
	 * @return the playlist data object storing information about the operation
	 *         result.
	 * @throws LockerServiceException
	 *             if an error occurred processing the locker response.
	 */
	public PlaylistData renamePlaylist(String playlistId, String newTitle)
			throws LockerServiceException {
		Map params = new HashMap();
		params.put("sid", loginData.getSessionId());
		params.put("playlist_id", playlistId);
		params.put("title", newTitle);
		Document doc = executeGeneral("playlistEdit", params);
		return new PlaylistData(doc.getRootElement());
	}
	
	/**
	 * Extracts the total number of result sets from a response document.
	 * 
	 * @param response   a response document.
	 * @return   the total number of result sets extracted 
	 */ 
	private int getTotalResultSets(Document response) {
		return LockerDataUtils.getInt(response.getRootElement(), "summary/totalResultSets", 0);
	}

	/**
	 * Navigates a XML document through an XPath and, for each encountered
	 * element, creates a specific Locker data object (<i>Artist</i>, <i>Album</i>,
	 * <i>Playlist</i>, or <i>Track</i>). Java reflection errors are silently
	 * ignored.
	 * 
	 * @param doc
	 *            the XML document being accessed.
	 * @param path
	 *            the XPath used to find specific sub-elements.
	 * @param dataClass
	 *            the class of objects being instantiated (<i>Artist</i>,
	 *            <i>Album</i>, <i>Playlist</i>, or <i>Track</i>).
	 * @return a list of <i>dataClass</i> objects.
	 */
	private List bind(Document doc, String path, Class dataClass) {
		return bind(doc.getRootElement(), path, dataClass);
	}

	/**
	 * Navigates a XML element through an XPath and, for each encountered
	 * element, creates a specific Locker data object (<i>Artist</i>, <i>Album</i>,
	 * <i>Playlist</i>, or <i>Track</i>). Java reflection errors are silently
	 * ignored.
	 * 
	 * @param elem
	 *            the XML element being accessed.
	 * @param path
	 *            the XPath used to find specific sub-elements.
	 * @param dataClass
	 *            the class of objects being instantiated (<i>Artist</i>,
	 *            <i>Album</i>, <i>Playlist</i>, or <i>Track</i>).
	 * @return a list of <i>dataClass</i> objects.
	 */
	private List bind(Element elem, String path, Class dataClass) {
		Constructor constr;
		try {
			constr = dataClass.getConstructor(ELEM_CLASS_ARRAY);
		} catch (SecurityException e) {
			return Collections.EMPTY_LIST;
		} catch (NoSuchMethodException e) {
			return Collections.EMPTY_LIST;
		}
		List resultList = new ArrayList();
		for (Iterator i = elem.selectNodes(path).iterator(); i.hasNext();) {
			Element item = (Element) i.next();
			try {
				resultList.add(constr.newInstance(new Object[] { item }));
			} catch (IllegalArgumentException e) {
			} catch (InstantiationException e) {
			} catch (IllegalAccessException e) {
			} catch (InvocationTargetException e) {
			}
		}
		return resultList;
	}

}
